<?php
/**
 * Created by PhpStorm.
 * User: Appla
 * Date: 2017/5/16
 * Time: 10:43
 */

namespace test;

use DateTime;
use Redis;
use SplQueue;
use artisan\cache;

/**
 * 统计Curl请求的情况记录HTTP状态和Curl请求状态
 * HTTP status codes:
 * @see https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 * @see https://curl.haxx.se/libcurl/c/libcurl-errors.html
 * curl error code最大没有超过100,所以直接和HTTP状态码混用了
 * Class CurlRequestLogger
 * @package test
 */
class CurlRequestLogger
{
    const CACHE_KEY_TPL = 'CurlRequestLogs:url:%s:%s';

    const CACHE_KEY_PREFIX = 'CurlRequestLogs:url:';

    const CACHE_KEY_TIMEOUT_TAG = 'requestTimeoutOptionLog';

    const CACHE_KEY_REQUEST_TIME_TAG = 'requestTimeCostLog';

    const CACHE_KEY_REQUEST_CALLER_URI_TAG = 'requestCallerUri';

    const CACHE_KEY_VISITOR_IP_TAG = 'visitorIp';

    const STR_SEPARATOR = '|';

    /**
     * 原始信息队列key
     */
    const REQUEST_INFO_QUEUE = 'CurlRequestLogs:originalInfo:queue';

    const CACHE_INSTANCE_ID = 'base2';

    const CACHE_INSTANCE_DB = 2;

    const REDIS_CONFIG = [
        '127.0.0.1',
        '6379',
        //        '192.168.1.77',
        //        6380,
    ];

    const TIME_FORMAT = 'YmdHi';

    const MAX_TTL = 3600;

    /**
     * 是否记录快宝内部请求, 暂时启用
     */
    const LOG_KB_INTERNAL_REQUEST = true;
    const KB_URI_SIGN = 'kuaidihelp.com';

    /**
     * 记录模式
     * 1.完全入队列
     * 2.解析后一次写入
     * 4.解析后马上写入
     */
    const MODE_TO_QUEUE = 1;
    const MODE_LAZY_WRITE = 2;
    const MODE_INSTANT_WRITE = 4;

    /**
     * 默认记录模式为批量写入
     * @var int
     */
    private $writeMode = self::MODE_LAZY_WRITE;

    /**
     * @var self
     */
    private static $instance;

    /**
     * @var \artisan\cache\PHPredisDriver|\Redis
     */
    private static $cacheInstance;

    /**
     * @var SplQueue
     */
    private static $queue;

    /**
     * @var bool
     */
    private static $logEnabled = true;

    /**
     * @var array
     */
    private $curlInfo;

    /**
     * @var int
     */
    private $instantiatedTime;

    /**
     * @var array
     */
    private $logData = [];

    /**
     * 记录每次请求的时间,用以计算访问时间分布
     * @var array
     */
    private $timeLogData = [];

    /**
     * CurlRequestLogger constructor.
     */
    protected function __construct()
    {
        $this->instantiatedTime = time();
        $this->writeMode !== self::MODE_INSTANT_WRITE && self::$queue = new SplQueue();
    }

    /**
     * @param resource $ch
     * @return array|mixed
     */
    public static function getCurlInfo($ch)
    {
        $info = [];
        if (is_resource($ch) && $curlInfo = curl_getinfo($ch)) {
            $curlInfo['error_info'] = curl_error($ch);
            $curlInfo['error_code'] = curl_errno($ch);
            $info = $curlInfo;
        }
        return $info;
    }

    /**
     * @param array|resource $info
     * @param array $requestInfo
     * @param string|null $response  response数据太大影响性能
     * @return bool|int
     */
    public static function log($info, array $requestInfo = [], $response = null)
    {
        if (self::$logEnabled) {
            if (is_resource($info)) {
                $info = self::getCurlInfo($info);
            }
            if (!empty($info['url']) && self::isAllowedUrl($info['url'])) {
                $requestInfo += [
                    'time_cost' => isset($info['total_time']) ? $info['total_time'] : 0,
                    'timeout'   => 0,
                    'visitor_ip' => self::getIp(),
                    'caller_uri' => self::getFullUri(),
                ];
                $info += $requestInfo;
                $info['response'] = $response;
//                $info['response_format_type'] = self::isXmlOrJson($response) ? 'valid_xml_or_json' : 'invalid_xml_or_json';
                return self::getInstance()->init($info)->write();
            }
        }
        return 0;
    }

    /**
     * @param string $url
     * @return bool
     */
    private static function isAllowedUrl($url) {
        return !empty($url) && (self::LOG_KB_INTERNAL_REQUEST || stripos($url, self::KB_URI_SIGN) === false);
    }

    /**
     * 真实IP
     * 避免伪造IP,优先使用REMOTE_ADDR,如果得到地址为私有网络IP,使用HTTP_X_FORWARDED_FOR的最后一个IP.
     * nginx的设置基本排除了大部分错误.如需过滤非公用地址,可增加filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)做条件, 暂时增加100.64.0.0/10的检查
     * @return string
     */
    private static function getIp()
    {
        static $ip = null;
        if ($ip === null){
            $ip = empty($_SERVER['REMOTE_ADDR']) ? getenv('REMOTE_ADDR') ?: 'unknown' : $_SERVER['REMOTE_ADDR'];
            if((strpos($ip, '100.') === 0 && preg_match('#^100\.(?:6[4-9]|[7-9]\d|1[0-1]\d|12[0-7])\.#', $ip)) || $ip === 'unknown') {
                if($xForwardedForIps = empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? getenv('HTTP_X_FORWARDED_FOR') ?: '' : $_SERVER['HTTP_X_FORWARDED_FOR']){
                    $xForwardedForIps = explode(',', $xForwardedForIps);
                    $ip = trim(array_pop($xForwardedForIps));
                }
            }
        }
        return $ip;
    }

    /**
     * @return string
     */
    private static function getFullUri()
    {
        return ($_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME']) .'/' . $_SERVER['REQUEST_URI'];
    }

//Prefix:url as key
//
//status_code_?
//time_cost
//total_hit
//max_request_time
//min_request_time
//time_log: string|string|
//content_type
//200时记录
//Content_status
//200时记录

    /**
     * @param array $curlInfo
     * @return array
     * @TODO 调整为返回一个可以写入对象
     */
    private function prepareData(array $curlInfo)
    {
        $data = [];
        if ($curlInfo) {
            $cacheKey = $this->buildCacheKey();
            $data = &$this->logData[$cacheKey];
            $status_code = $curlInfo['http_code'] ?: $curlInfo['error_code'];
            $curlInfo['time_cost'] && ($this->timeLogData[$cacheKey][] = $curlInfo['time_cost']);
            $status_code_key = sprintf('status_code_%s', $status_code);
            //http状态码
            isset($data[$status_code_key]) && $data[$status_code_key]++ || $data[$status_code_key] = 1;
            //解析时加吧
//            $curlInfo['error_code'] === 28 && ($data['error_timeout'] = isset($data['error_timeout']) ? ++$data['error_timeout'] : 1);
            //耗时
            isset($data['time_cost']) && ($data['time_cost'] += $curlInfo['time_cost']) || $data['time_cost'] = $curlInfo['time_cost'];
            $data['timeout'] = $curlInfo['timeout'];
            isset($data['total_hit']) && $data['total_hit']++ || $data['total_hit'] = 1;
            //正常返回, xml或者json
            $responseKeyType = self::isXmlOrJson($curlInfo['response']) ? 'valid_xml_or_json' : 'invalid_xml_or_json';
//            $responseKeyType = $curlInfo['response_format_type'];
            isset($data[$responseKeyType]) && $data[$responseKeyType]++ || $data[$responseKeyType] = 1;
            $data['cache_key'] = $cacheKey;
        }
        return $data;
    }

    /**
     * @param string $str
     * @return bool
     */
    private static function isXmlOrJson($str)
    {
        return self::isValidJsonStr($str) || self::isValidXmlStr($str);
    }

    /**
     * @param string $str
     * @return bool
     */
    private static function isValidXmlStr($str)
    {
        return is_string($str) && stripos(trim($str), '<?xml') === 0;
    }

    /**
     * @TODO 假定接口返回格式为数组或者对象或者字符串,预过滤数据提高性能??? && preg_match('~^([\[{"])~', $data)
     * @param mixed $data
     * @return bool
     */
    private static function isValidJsonStr($data)
    {
        $return = false;
        if (is_string($data)) {
            json_decode($data);
            $return = json_last_error() === JSON_ERROR_NONE;
        } elseif (is_numeric($data)) {
            $return = true;
        }
        return $return;
    }

    /**
     * @return array|int
     */
    private function write()
    {
        $ret = 1;
        switch ($this->writeMode) {
            case self::MODE_TO_QUEUE:
                self::$queue->enqueue($this->curlInfo + ['cache_key' => $this->buildCacheKey()]);
                break;
            case self::MODE_INSTANT_WRITE:
                $ret = $this->instantWrite([$this->prepareData($this->curlInfo)]);
                break;
            case self::MODE_LAZY_WRITE:
            default:
                self::$queue->enqueue($this->prepareData($this->curlInfo));
        }
        return $ret;
    }

    /**
     * @param iterable $data
     * @return array
     */
    private function instantWrite(iterable $data)
    {
        $pipe = self::getCacheInstance()->pipeline();
        foreach ($data as $datum) {
            $mainKey = $datum['cache_key'];
            //访问时间分布需要单独记录, 超时单独记录
            $pipe->append($mainKey . ':' . self::CACHE_KEY_REQUEST_TIME_TAG, self::STR_SEPARATOR . $datum['time_cost']);
            $pipe->append($mainKey . ':' . self::CACHE_KEY_TIMEOUT_TAG, self::STR_SEPARATOR . $datum['timeout']);
//            $pipe->append($mainKey . ':' . self::CACHE_KEY_VISITOR_IP_TAG, self::STR_SEPARATOR . $datum['visitor_ip']);
//            $pipe->append($mainKey . ':' . self::CACHE_KEY_REQUEST_CALLER_URI_TAG, self::STR_SEPARATOR . $datum['caller_uri']);
            unset($datum['cache_key']); // $datum['timeout']
            foreach ($datum as $key => $item) {
                $pipe->hincrByFloat($mainKey, $key, (float)$item);
            }
        }
        return $pipe->exec();
    }

    /**
     * @param iterable $data
     * @return array
     */
    private function pushToQueue(iterable $data)
    {
        $pipe = self::getCacheInstance()->pipeline();
        foreach ($data as $datum) {
            $pipe->lPush(self::REQUEST_INFO_QUEUE, json_encode($datum));
        }
        return $pipe->exec();
    }

    /**
     * @return \artisan\cache\PHPredisDriver|\Redis
     */
    private static function getCacheInstance()
    {
        return self::$cacheInstance ?: self::$cacheInstance = (function () {
            $redis = new Redis();
            $redis->connect(...self::REDIS_CONFIG);
            $redis->select(self::CACHE_INSTANCE_DB);
            return $redis;
        })();
//        return self::$cacheInstance ?: self::$cacheInstance = cache::connect(self::CACHE_INSTANCE_ID);
    }

    /**
     * @return string
     */
    private function buildCacheKey()
    {
        return sprintf(self::CACHE_KEY_TPL, $this->getUrl(), $this->getTimeStr());
    }

    /**
     * @return string
     */
    private function getUrl()
    {
        return self::parseUrl($this->curlInfo['url']);
    }

    /**
     * @param $url
     * @return string
     */
    public static function parseUrl($url)
    {
        //$urlArr = parse_url($url);
        return trim(preg_replace('~^https?://|\?.*$~i', '', $url), '/');
    }

    /**
     * @return string
     */
    private function getTimeStr()
    {
        return (new DateTime())->format(self::TIME_FORMAT);
    }

    /**
     * @param array $curlInfo
     * @return $this
     */
    private function init(array $curlInfo)
    {
        $this->curlInfo = $curlInfo;
        return $this;
    }

    /**
     * @return CurlRequestLogger
     */
    private static function getInstance()
    {
        return self::$instance ?: self::$instance = new self();
    }

    /**
     * @return string
     */
    private function getUrlHash()
    {
        return md5($this->getUrl());
    }

    /**
     * 延迟写入
     */
    public function __destruct()
    {
        if (!self::$queue->isEmpty()) {
            $this->writeMode === self::MODE_TO_QUEUE && $this->pushToQueue(self::$queue);
            $this->writeMode === self::MODE_LAZY_WRITE && $this->instantWrite(self::$queue);
        }
    }
}


//$url = 'http://www.baidu.com';
//$ch = curl_init($url);
//$start = microtime(true);
//curl_setopt($ch, CURLOPT_TIMEOUT, 1);
//curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
//curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
//$ret = curl_exec($ch);
//$end = microtime(true);
//$aa = CurlRequestLogger::log($ch, ['timeout' => 5, 'time_cost' => $end - $start, 'url' => $url], $ret);
//$a = curl_getinfo($ch);
//curl_close($ch);
//
////
//$url = 'http://www.sina.com';
//$ch = curl_init($url);
//$start = microtime(true);
//curl_setopt($ch, CURLOPT_TIMEOUT, 1);
//curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
//curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
//$ret = curl_exec($ch);
//$end = microtime(true);
//$aa = CurlRequestLogger::log($ch, ['timeout' => 5, 'time_cost' => $end - $start, 'url' => $url], $ret);
//$a = curl_getinfo($ch);
//curl_close($ch);
//
////
//$url = 'http://t.com/timeout.php';
//$ch = curl_init($url);
//$start = microtime(true);
//curl_setopt($ch, CURLOPT_TIMEOUT, 1);
//curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
//curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
//$ret = curl_exec($ch);
//$end = microtime(true);
//$aa = CurlRequestLogger::log($ch, ['timeout'   => 1,
//                                   'time_cost' => $end - $start,
//                                   'url'       => $url,
//], $ret);
//$a = curl_getinfo($ch);
//curl_close($ch);
